/*
    This file is part of Sylvan.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Sylvan.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <QAction>
#include <QHBoxLayout>
#include <QMenu>
#include <QMenuBar>
#include <QStatusBar>
#include <QToolBar>
#include <QDockWidget>
#include <QTreeView>
#include <QMessageBox>
#include <QFileDialog>
#include <QInputDialog>
#include <QClipboard>
#include <QWindow>
#include <QSettings>
#include <QDesktopWidget>

#include <board/boardfactory.h>
#include <chessgame.h>
#include <timecontrol.h>
#include <enginebuilder.h>
#include <enginemanager.h>
#include <gamemanager.h>
#include <playerbuilder.h>
#include <chessplayer.h>
#include <humanbuilder.h>
#include <tournament.h>
#include <pgnstream.h>

#include "sylvanapp.h"
#include "gameviewer.h"
#include "gametabbar.h"
#include "movelist.h"
#include "chessclock.h"
#include "plaintextlog.h"
#include "gamedatabasemanager.h"
#include "pgngameentry.h"
#include "pgntagsmodel.h"
#include "evalhistory.h"
#include "evalwidget.h"
#include "boardview/boardscene.h"
#include "newgamedlg.h"
#include "newtournamentdlg.h"
#include "tournamentresultsdlg.h"
#include "mainwindow.h"

#ifdef QT_DEBUG
#include <modeltest.h>
#endif

MainWindow::TabData::TabData(ChessGame* game, Tournament* tournament)
    : m_id(game),
      m_game(game),
      m_pgn(game->pgn()),
      m_tournament(tournament),
      m_finished(false)
{
}

MainWindow::MainWindow(ChessGame* game)
    : m_game(nullptr),
      m_closing(false),
      m_readyToClose(false),
      m_firstTabAutoCloseEnabled(false)
{
    setAttribute(Qt::WA_DeleteOnClose, true);
    setDockNestingEnabled(true);

    m_gameViewer = new GameViewer(Qt::Horizontal, nullptr, true);
    for (int i = 0; i < 2; i++)
    {
        Chess::Side side = Chess::Side::Type(i);
        m_gameViewer->chessClock(side)->setPlayerName(side.toString());
    }
    m_gameViewer->setContentsMargins(6, 6, 6, 6);

    m_moveList = new MoveList(this);
    m_tagsModel = new PgnTagsModel(this);
#ifdef QT_DEBUG
    new ModelTest(m_tagsModel, this);
#endif

    m_evalHistory = new EvalHistory(this);
    m_evalWidgets[0] = new EvalWidget(this);
    m_evalWidgets[1] = new EvalWidget(this);

    QVBoxLayout* mainLayout = new QVBoxLayout();
    mainLayout->addWidget(m_gameViewer);

    // The content margins look stupid when used with dock widgets
    mainLayout->setContentsMargins(0, 0, 0, 0);

    QWidget* mainWidget = new QWidget(this);
    mainWidget->setLayout(mainLayout);
    setCentralWidget(mainWidget);

    createActions();
    createMenus();
    createToolBars();
    createDockWindows();

    statusBar()->showMessage("");

    connect(m_moveList, SIGNAL(moveClicked(int,bool)),
            m_gameViewer, SLOT(viewMove(int,bool)));
    connect(m_moveList, SIGNAL(commentClicked(int, QString)),
            this, SLOT(editMoveComment(int, QString)));
    connect(m_gameViewer, SIGNAL(moveSelected(int)),
            m_moveList, SLOT(selectMove(int)));

    connect(SylvanApplication::instance()->gameManager(),
            SIGNAL(finished()), this, SLOT(onGameManagerFinished()),
            Qt::QueuedConnection);

    readSettings();
    addGame(game);
}

MainWindow::~MainWindow()
{
}

void MainWindow::createActions()
{
    m_newGameAct = new QAction(tr("&New..."), this);
    m_newGameAct->setShortcut(QKeySequence::New);

    m_openPgnAct = new QAction(tr("Open PGN"), this);
    m_openPgnAct->setShortcut(QKeySequence::Open);

    m_closeGameAct = new QAction(tr("&Close"), this);
#ifdef Q_OS_WIN32
    m_closeGameAct->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_W));
#else
    m_closeGameAct->setShortcut(QKeySequence::Close);
#endif

    m_saveGameAct = new QAction(tr("&Save"), this);
    m_saveGameAct->setShortcut(QKeySequence::Save);

    m_saveGameAsAct = new QAction(tr("&Save As..."), this);
    m_saveGameAsAct->setShortcut(QKeySequence::SaveAs);

    m_copyFenAct = new QAction(tr("&Copy FEN"), this);
    QAction* copyFenSequence = new QAction(m_gameViewer);
    copyFenSequence->setShortcut(QKeySequence::Copy);
    copyFenSequence->setShortcutContext(Qt::WidgetWithChildrenShortcut);
    m_gameViewer->addAction(copyFenSequence);

    m_pasteFenAct = new QAction(tr("&Paste FEN"), this);
    m_pasteFenAct->setShortcut(QKeySequence(QKeySequence::Paste));

    m_copyPgnAct = new QAction(tr("&Copy PGN"), this);

    m_flipBoardAct = new QAction(tr("&Flip The Board Up And Down"), this);

    m_adjudicateDrawAct = new QAction(tr("&Forced Draw"), this);
    m_adjudicateRedWinAct = new QAction(tr("Forced Red Win"), this);
    m_adjudicateBlackWinAct = new QAction(tr("Forced Black Win"), this);

    m_resignGameAct = new QAction(tr("Give Up"), this);

    m_quitGameAct = new QAction(tr("&Quit"), this);
    m_quitGameAct->setMenuRole(QAction::QuitRole);
#ifdef Q_OS_WIN32
    m_quitGameAct->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Q));
#else
    m_quitGameAct->setShortcut(QKeySequence::Quit);
#endif

    m_newTournamentAct = new QAction(tr("&New Tournament..."), this);
    m_stopTournamentAct = new QAction(tr("&End Tournament"), this);
    m_showTournamentResultsAct = new QAction(tr("&Tournament Report..."), this);

    m_showSettingsAct = new QAction(tr("&General Settings"), this);
    m_showSettingsAct->setMenuRole(QAction::PreferencesRole);

    m_showGameDatabaseWindowAct = new QAction(tr("&Game Database"), this);

    m_showGameWallAct = new QAction(tr("&Current Game"), this);

    m_minimizeAct = new QAction(tr("&Minimize"), this);
    m_minimizeAct->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_M));

    m_showPreviousTabAct = new QAction(tr("&Show Previous Game"), this);
#ifdef Q_OS_MAC
    m_showPreviousTabAct->setShortcut(QKeySequence(Qt::MetaModifier + Qt::ShiftModifier + Qt::Key_Tab));
#else
    m_showPreviousTabAct->setShortcut(QKeySequence(Qt::ControlModifier + Qt::ShiftModifier + Qt::Key_Tab));
#endif

    m_showNextTabAct = new QAction(tr("&Show Next Game"), this);
#ifdef Q_OS_MAC
    m_showNextTabAct->setShortcut(QKeySequence(Qt::MetaModifier + Qt::Key_Tab));
#else
    m_showNextTabAct->setShortcut(QKeySequence(Qt::ControlModifier + Qt::Key_Tab));
#endif

    m_aboutAct = new QAction(tr("&About..."), this);
    m_aboutAct->setMenuRole(QAction::AboutRole);

    connect(m_newGameAct, SIGNAL(triggered()), this, SLOT(newGame()));

    connect(m_openPgnAct, SIGNAL(triggered()), this, SLOT(OpenPgnGame()));

    connect(m_copyFenAct, SIGNAL(triggered()), this, SLOT(copyFen()));
    connect(m_pasteFenAct, SIGNAL(triggered()), this, SLOT(pasteFen()));
    connect(copyFenSequence, SIGNAL(triggered()), this, SLOT(copyFen()));
    connect(m_copyPgnAct, SIGNAL(triggered()), this, SLOT(copyPgn()));
    connect(m_flipBoardAct, SIGNAL(triggered()),
            m_gameViewer->boardScene(), SLOT(flip()));
    connect(m_closeGameAct, &QAction::triggered, this, [=]()
    {
        auto focusWindow = SylvanApplication::activeWindow();
        if (!focusWindow)
            return;

        auto focusMainWindow = qobject_cast<MainWindow*>(focusWindow);
        if (focusMainWindow)
        {
            focusMainWindow->closeCurrentGame();
            return;
        }

        focusWindow->close();
    });

    auto app = SylvanApplication::instance();

    connect(m_saveGameAct, SIGNAL(triggered()), this, SLOT(save()));
    connect(m_saveGameAsAct, SIGNAL(triggered()), this, SLOT(saveAs()));

    connect(m_adjudicateDrawAct, SIGNAL(triggered()), this, SLOT(adjudicateDraw()));
    connect(m_adjudicateRedWinAct, SIGNAL(triggered()), this, SLOT(adjudicateRedWin()));
    connect(m_adjudicateBlackWinAct, SIGNAL(triggered()), this, SLOT(adjudicateBlackWin()));

    connect(m_resignGameAct, SIGNAL(triggered()), this, SLOT(resignGame()));
    connect(m_quitGameAct, &QAction::triggered,
            app, &SylvanApplication::onQuitAction);

    connect(m_newTournamentAct, SIGNAL(triggered()), this, SLOT(newTournament()));

    connect(m_minimizeAct, &QAction::triggered, this, [=]()
    {
        auto focusWindow = app->focusWindow();
        if (focusWindow != nullptr)
        {
            focusWindow->showMinimized();
        }
    });

    connect(m_showSettingsAct, SIGNAL(triggered()),
            app, SLOT(showSettingsDialog()));

    connect(m_showTournamentResultsAct, SIGNAL(triggered()),
            app, SLOT(showTournamentResultsDialog()));

    connect(m_showGameDatabaseWindowAct, SIGNAL(triggered()),
            app, SLOT(showGameDatabaseDialog()));

    connect(m_showGameWallAct, SIGNAL(triggered()),
            app, SLOT(showGameWall()));

    connect(m_aboutAct, SIGNAL(triggered()), this, SLOT(showAboutDialog()));
}

void MainWindow::createMenus()
{
    m_gameMenu = menuBar()->addMenu(tr("&Game"));
    m_gameMenu->addAction(m_newGameAct);
    m_gameMenu->addAction(m_openPgnAct);
    m_gameMenu->addSeparator();
    m_gameMenu->addAction(m_closeGameAct);
    m_gameMenu->addAction(m_saveGameAct);
    m_gameMenu->addAction(m_saveGameAsAct);
    m_gameMenu->addSeparator();
    m_gameMenu->addAction(m_copyFenAct);
    m_gameMenu->addAction(m_copyPgnAct);
    m_gameMenu->addAction(m_pasteFenAct);
    m_gameMenu->addSeparator();
    m_gameMenu->addAction(m_adjudicateDrawAct);
    m_gameMenu->addAction(m_adjudicateRedWinAct);
    m_gameMenu->addAction(m_adjudicateBlackWinAct);
    m_gameMenu->addSeparator();
    m_gameMenu->addAction(m_resignGameAct);
    m_gameMenu->addSeparator();
    m_gameMenu->addAction(m_quitGameAct);

    m_tournamentMenu = menuBar()->addMenu(tr("&Tournament"));
    m_tournamentMenu->addAction(m_newTournamentAct);
    m_tournamentMenu->addAction(m_stopTournamentAct);
    m_tournamentMenu->addAction(m_showTournamentResultsAct);
    m_stopTournamentAct->setEnabled(false);

    m_toolsMenu = menuBar()->addMenu(tr("&Setting"));
    m_toolsMenu->addAction(m_showSettingsAct);
    m_toolsMenu->addAction(m_showGameDatabaseWindowAct);

    m_viewMenu = menuBar()->addMenu(tr("&View"));
    m_viewMenu->addAction(m_flipBoardAct);
    m_viewMenu->addSeparator();

    m_windowMenu = menuBar()->addMenu(tr("&Window"));
    addDefaultWindowMenu();

    connect(m_windowMenu, SIGNAL(aboutToShow()), this,
            SLOT(onWindowMenuAboutToShow()));

    m_helpMenu = menuBar()->addMenu(tr("&Help"));
    m_helpMenu->addAction(m_aboutAct);
}

void MainWindow::createToolBars()
{
    m_tabBar = new GameTabBar();
    m_tabBar->setDocumentMode(true);
    m_tabBar->setTabsClosable(true);
    m_tabBar->setExpanding(false);
    m_tabBar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);

    connect(m_tabBar, SIGNAL(currentChanged(int)),
            this, SLOT(onTabChanged(int)));
    connect(m_tabBar, SIGNAL(tabCloseRequested(int)),
            this, SLOT(onTabCloseRequested(int)));
    connect(m_showPreviousTabAct, SIGNAL(triggered()),
            m_tabBar, SLOT(showPreviousTab()));
    connect(m_showNextTabAct, SIGNAL(triggered()),
            m_tabBar, SLOT(showNextTab()));

    QToolBar* toolBar = new QToolBar(tr("Game Tabs"));
    toolBar->setObjectName("GameTabs");
    toolBar->setAllowedAreas(Qt::TopToolBarArea);
    toolBar->addWidget(m_tabBar);
    addToolBar(toolBar);


    //QAction* m_engineAnalyzeAct;

    this->m_linkChessBoardAct = new QAction(this);
    this->m_linkChessBoardAct->setObjectName(QStringLiteral("LinkChessBoard"));
    //QIcon iconLinkChessBoard;
    //iconLinkChessBoard.addFile(QStringLiteral(":/icon/Links.ico"),
    //	QSize(), QIcon::Normal, QIcon::Off);
    //this->m_linkChessBoardAct->setIcon(iconLinkChessBoard);
    this->m_linkChessBoardAct->setText(tr("Link"));
    this->m_linkChessBoardAct->setToolTip(tr("Link other boards."));

    this->m_engineThinkAct = new QAction(this);
    this->m_engineThinkAct->setObjectName(QStringLiteral("EngineThink"));
    //QIcon iconEngineThink;
    //iconEngineThink.addFile(QStringLiteral(":/icon/thought-balloon.ico"),
    //	QSize(), QIcon::Normal, QIcon::Off);
    //this->m_engineThinkAct->setIcon(iconEngineThink);
    this->m_engineThinkAct->setText(tr("Think"));
    this->m_engineThinkAct->setToolTip(tr("Make the engine think about the current game and move automatically."));

    this->m_engineAnalyzeAct = new QAction(this);
    this->m_engineAnalyzeAct->setObjectName(QStringLiteral("EngineAnalyze"));
    QIcon iconEngineAnalyze;
    iconEngineAnalyze.addFile(QStringLiteral(":/icon/gnome-zoom-fit-best.ico"),
        QSize(), QIcon::Normal, QIcon::Off);
    this->m_engineAnalyzeAct->setIcon(iconEngineAnalyze);
    this->m_engineAnalyzeAct->setText(tr("Analyze"));
    this->m_engineAnalyzeAct->setToolTip(tr("Let the engine analyze the current game."));

    this->m_engineStopAct = new QAction(this);
    this->m_engineStopAct->setObjectName(QStringLiteral("EngineStop"));
    QIcon iconEngineStop;
    iconEngineStop.addFile(QStringLiteral(":/icon/gnome-media-playback-stop.ico"),
                           QSize(), QIcon::Normal, QIcon::Off);
    this->m_engineStopAct->setIcon(iconEngineStop);
    this->m_engineStopAct->setText(tr("Stop"));
    this->m_engineStopAct->setToolTip(tr("Let the engine stop thinking."));

    this->m_engineSettingAct = new QAction(this);
    this->m_engineSettingAct->setObjectName(QStringLiteral("EngineSetting"));
    QIcon iconEngineSetting;
    iconEngineSetting.addFile(QStringLiteral(":/icon/gnome-preferences-system.ico"),
                              QSize(), QIcon::Normal, QIcon::Off);
    this->m_engineSettingAct->setIcon(iconEngineSetting);
    this->m_engineSettingAct->setText(tr("Setting"));
    this->m_engineSettingAct->setToolTip(tr("Setup engine."));

    this->m_aboutAct = new QAction(this);
    this->m_aboutAct->setObjectName(QStringLiteral("About"));
    QIcon iconAbout;
    iconAbout.addFile(QStringLiteral(":/icon/dialog-information.ico"),
                      QSize(), QIcon::Normal, QIcon::Off);
    this->m_aboutAct->setIcon(iconAbout);
    this->m_aboutAct->setText(tr("About"));
    this->m_aboutAct->setToolTip(tr("About..."));

    this->m_toolBar = new QToolBar(this);
    this->m_toolBar->setObjectName(QStringLiteral("m_toolBar"));
    this->m_toolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);

    /*this->m_toolBar->setMovable(false);
    this->m_toolBar->setAllowedAreas(Qt::TopToolBarArea);*/

    this->addToolBar(Qt::TopToolBarArea, this->m_toolBar);

    /*this->m_toolBar->addAction(this->m_linkChessBoardAct);
    this->m_toolBar->addAction(this->m_engineThinkAct);
    this->m_toolBar->addAction(this->m_engineStopAct);
    this->m_toolBar->addAction(this->m_engineAnalyzeAct);*/
    this->m_toolBar->addAction(this->m_engineSettingAct);
    this->m_toolBar->addAction(this->m_aboutAct);

    auto app = SylvanApplication::instance();

    connect(this->m_engineAnalyzeAct, &QAction::triggered, this, &MainWindow::onEngineAnalyze);
    connect(this->m_engineSettingAct, SIGNAL(triggered()), app, SLOT(showSettingsDialog()));
    connect(this->m_aboutAct, &QAction::triggered, this, &MainWindow::showAboutDialog);
}

void MainWindow::createDockWindows()
{
    // Engine debug
    QDockWidget* engineDebugDock = new QDockWidget(tr("Engine Debug"), this);
    m_engineDebugLog = new PlainTextLog(engineDebugDock);
    engineDebugDock->setObjectName("EngineDebugDock");
    engineDebugDock->setWidget(m_engineDebugLog);
    engineDebugDock->close();
    addDockWidget(Qt::BottomDockWidgetArea, engineDebugDock);

    // Evaluation history
    auto evalHistoryDock = new QDockWidget(tr("Historical Evaluation Curve"), this);
    evalHistoryDock->setObjectName("EvalHistoryDock");
    evalHistoryDock->setWidget(m_evalHistory);
    addDockWidget(Qt::BottomDockWidgetArea, evalHistoryDock);

    // Players' eval widgets
    auto redEvalDock = new QDockWidget(tr("Red Score"), this);
    redEvalDock->setObjectName("RedEvalDock");
    redEvalDock->setWidget(m_evalWidgets[Chess::Side::Red]);
    addDockWidget(Qt::RightDockWidgetArea, redEvalDock);
    auto blackEvalDock = new QDockWidget(tr("Black Score"), this);
    blackEvalDock->setObjectName("BlackEvalDock");
    blackEvalDock->setWidget(m_evalWidgets[Chess::Side::Black]);
    addDockWidget(Qt::RightDockWidgetArea, blackEvalDock);

    // Move list
    QDockWidget* moveListDock = new QDockWidget(tr("Move List"), this);
    moveListDock->setObjectName("MoveListDock");
    moveListDock->setWidget(m_moveList);
    addDockWidget(Qt::RightDockWidgetArea, moveListDock);
    splitDockWidget(moveListDock, redEvalDock, Qt::Horizontal);
    splitDockWidget(redEvalDock, blackEvalDock, Qt::Vertical);

    // Tags
    QDockWidget* tagsDock = new QDockWidget(tr("Tags"), this);
    tagsDock->setObjectName("TagsDock");
    QTreeView* tagsView = new QTreeView(tagsDock);
    tagsView->setModel(m_tagsModel);
    tagsView->setAlternatingRowColors(true);
    tagsView->setRootIsDecorated(false);
    tagsDock->setWidget(tagsView);

    addDockWidget(Qt::RightDockWidgetArea, tagsDock);

    tabifyDockWidget(moveListDock, tagsDock);
    moveListDock->raise();

    // Add toggle view actions to the View menu
    m_viewMenu->addAction(moveListDock->toggleViewAction());
    m_viewMenu->addAction(tagsDock->toggleViewAction());
    m_viewMenu->addAction(engineDebugDock->toggleViewAction());
    m_viewMenu->addAction(evalHistoryDock->toggleViewAction());
    m_viewMenu->addAction(redEvalDock->toggleViewAction());
    m_viewMenu->addAction(blackEvalDock->toggleViewAction());
}

void MainWindow::readSettings()
{
    QSettings s;
    s.beginGroup("ui");
    s.beginGroup("mainwindow");

    restoreGeometry(s.value("geometry").toByteArray());
    // Workaround for https://bugreports.qt.io/browse/QTBUG-16252
    if (isMaximized())
        setGeometry(QApplication::desktop()->availableGeometry(this));
    restoreState(s.value("window_state").toByteArray());

    s.endGroup();
    s.endGroup();
}

void MainWindow::writeSettings()
{
    QSettings s;
    s.beginGroup("ui");
    s.beginGroup("mainwindow");

    s.setValue("geometry", saveGeometry());
    s.setValue("window_state", saveState());

    s.endGroup();
    s.endGroup();
}

void MainWindow::addGame(ChessGame* game)
{
    Tournament* tournament = qobject_cast<Tournament*>(QObject::sender());

    TabData tab(game, tournament);

    if (tournament)
    {
        int index = tabIndex(tournament, true);
        if (index != -1)
        {
            delete m_tabs[index].m_pgn;
            m_tabs[index] = tab;

            m_tabBar->setTabText(index, genericTitle(tab));
            if (!m_closing && m_tabBar->currentIndex() == index)
                setCurrentGame(tab);

            return;
        }
    }
    else
        connect(game, SIGNAL(finished(ChessGame*)),
                this, SLOT(onGameFinished(ChessGame*)));

    m_tabs.append(tab);
    m_tabBar->setCurrentIndex(m_tabBar->addTab(genericTitle(tab)));

#if 0
    // close initial tab if unused and if enabled by settings
    if (m_tabs.size() <= 1
            &&  m_firstTabAutoCloseEnabled)
    {
        if (QSettings().value("ui/close_unused_initial_tab", true).toBool()
                &&  !m_tabs[0].m_game.isNull()
                &&  m_tabs[0].m_game.data()->moves().isEmpty())
            closeTab(0);

        m_firstTabAutoCloseEnabled = false;
    }

    if (m_tabs.size() >= 2)
        m_tabBar->parentWidget()->show();
#endif
}

void MainWindow::removeGame(int index)
{
    Q_ASSERT(index != -1);

    m_tabs.removeAt(index);
    m_tabBar->removeTab(index);

#if 0
    if (m_tabs.size() == 1)
        m_tabBar->parentWidget()->hide();
#endif
}

void MainWindow::destroyGame(ChessGame* game)
{
    Q_ASSERT(game != nullptr);

    int index = tabIndex(game);
    Q_ASSERT(index != -1);
    TabData tab = m_tabs.at(index);

    removeGame(index);

    if (tab.m_tournament == nullptr)
        game->deleteLater();
    delete tab.m_pgn;
    if (m_tabs.isEmpty())
        close();
}

void MainWindow::setCurrentGame(const TabData& gameData)
{
    if (gameData.m_game == m_game && m_game != nullptr)
        return;

    for (int i = 0; i < 2; i++)
    {
        ChessPlayer* player(m_players[i]);
        if (player != nullptr)
        {
            disconnect(player, nullptr, m_engineDebugLog, nullptr);
            disconnect(player, nullptr,
                       m_gameViewer->chessClock(Chess::Side::Red), nullptr);
            disconnect(player, nullptr,
                       m_gameViewer->chessClock(Chess::Side::Black), nullptr);
        }
    }

    if (m_game != nullptr)
    {
        m_game->pgn()->setTagReceiver(nullptr);
        m_gameViewer->disconnectGame();
        disconnect(m_game, nullptr, m_moveList, nullptr);

        ChessGame* tmp = m_game;
        m_game = nullptr;

        // QObject::disconnect() is not atomic, so we need to flush
        // all pending events from the previous game before switching
        // to the next one.
        tmp->lockThread();
        SylvanApplication::processEvents();
        tmp->unlockThread();

        // If the call to SylvanApplication::processEvents() caused
        // a new game to be selected as the current game, then our
        // work here is done.
        if (m_game != nullptr)
            return;
    }

    m_game = gameData.m_game;

    lockCurrentGame();

    m_engineDebugLog->clear();

    m_moveList->setGame(m_game, gameData.m_pgn);
    m_evalHistory->setGame(m_game);

    if (m_game == nullptr)
    {
        m_gameViewer->setGame(gameData.m_pgn);
        m_evalHistory->setPgnGame(gameData.m_pgn);

        for (int i = 0; i < 2; i++)
        {
            Chess::Side side = Chess::Side::Type(i);
            auto clock = m_gameViewer->chessClock(side);
            clock->stop();
            clock->setInfiniteTime(true);
            QString name = nameOnClock(gameData.m_pgn->playerName(side),
                                       side);
            clock->setPlayerName(name);
        }

        m_tagsModel->setTags(gameData.m_pgn->tags());

        updateWindowTitle();
        updateMenus();

        for (auto evalWidget : m_evalWidgets)
            evalWidget->setPlayer(nullptr);

        return;
    }
    else
        m_gameViewer->setGame(m_game);

    m_tagsModel->setTags(gameData.m_pgn->tags());
    gameData.m_pgn->setTagReceiver(m_tagsModel);

    for (int i = 0; i < 2; i++)
    {
        Chess::Side side = Chess::Side::Type(i);
        ChessPlayer* player(m_game->player(side));
        m_players[i] = player;

        connect(player, SIGNAL(debugMessage(QString)),
                m_engineDebugLog, SLOT(appendPlainText(QString)));

        auto clock = m_gameViewer->chessClock(side);

        clock->stop();
        QString name = nameOnClock(player->name(), side);
        clock->setPlayerName(name);
        connect(player, SIGNAL(nameChanged(QString)),
                clock, SLOT(setPlayerName(QString)));

        clock->setInfiniteTime(player->timeControl()->isInfinite());

        if (player->state() == ChessPlayer::Thinking)
            clock->start(player->timeControl()->activeTimeLeft());
        else
            clock->setTime(player->timeControl()->timeLeft());

        connect(player, SIGNAL(startedThinking(int)),
                clock, SLOT(start(int)));
        connect(player, SIGNAL(stoppedThinking()),
                clock, SLOT(stop()));
        m_evalWidgets[i]->setPlayer(player);
    }

    if (m_game->boardShouldBeFlipped())
        m_gameViewer->boardScene()->flip();

    updateMenus();
    updateWindowTitle();
    unlockCurrentGame();
}

int MainWindow::tabIndex(ChessGame* game) const
{
    Q_ASSERT(game != nullptr);

    for (int i = 0; i < m_tabs.size(); i++)
    {
        if (m_tabs.at(i).m_id == game)
            return i;
    }

    return -1;
}

int MainWindow::tabIndex(Tournament* tournament, bool freeTab) const
{
    Q_ASSERT(tournament != nullptr);

    for (int i = 0; i < m_tabs.size(); i++)
    {
        const TabData& tab = m_tabs.at(i);

        if (tab.m_tournament == tournament
                &&  (!freeTab || (tab.m_game == nullptr || tab.m_finished)))
            return i;
    }

    return -1;
}

void MainWindow::onTabChanged(int index)
{
    if (index == -1 || m_closing)
        m_game = nullptr;
    else
        setCurrentGame(m_tabs.at(index));
}

void MainWindow::onTabCloseRequested(int index)
{
    const TabData& tab = m_tabs.at(index);

    if (tab.m_tournament && tab.m_game)
    {
        auto btn = QMessageBox::question(this, tr("Stop Tournament"),
                                         tr("Do you really want to stop the current tournament?"));
        if (btn != QMessageBox::Yes)
            return;
    }

    closeTab(index);
}

void MainWindow::closeTab(int index)
{
    const TabData& tab = m_tabs.at(index);

    if (tab.m_game == nullptr)
    {
        delete tab.m_pgn;
        removeGame(index);

        if (m_tabs.isEmpty())
            close();

        return;
    }

    if (tab.m_finished)
        destroyGame(tab.m_game);
    else
    {
        connect(tab.m_game, SIGNAL(finished(ChessGame*)),
                this, SLOT(destroyGame(ChessGame*)));
        QMetaObject::invokeMethod(tab.m_game, "stop", Qt::QueuedConnection);
    }
}

void MainWindow::closeCurrentGame()
{
    closeTab(m_tabBar->currentIndex());
}

void MainWindow::newGame()
{
    EngineManager* engineManager = SylvanApplication::instance()->engineManager();
    NewGameDialog dlg(engineManager, this);
    if (dlg.exec() != QDialog::Accepted)
        return;

    auto game = dlg.createGame();
    if (!game)
    {
        QMessageBox::critical(this, tr("Could Not Initialize Game"),
                              tr("The game could not be initialized "
                                 "due to an invalid opening."));
        return;
    }

    PlayerBuilder* builders[2] = {
        dlg.createPlayerBuilder(Chess::Side::Red),
        dlg.createPlayerBuilder(Chess::Side::Black)
    };

    if (builders[game->board()->sideToMove()]->isHuman())
        game->pause();

    // Start the game in a new tab
    connect(game, SIGNAL(initialized(ChessGame*)),
            this, SLOT(addGame(ChessGame*)));
    connect(game, SIGNAL(startFailed(ChessGame*)),
            this, SLOT(onGameStartFailed(ChessGame*)));
    SylvanApplication::instance()->gameManager()->newGame(game,
                                                          builders[Chess::Side::Red], builders[Chess::Side::Black]);
}

void MainWindow::onGameStartFailed(ChessGame* game)
{
    QMessageBox::critical(this, tr("Game Error"), game->errorString());
}

void MainWindow::onGameFinished(ChessGame* game)
{
    int tIndex = tabIndex(game);
    if (tIndex == -1)
        return;

    auto& tab = m_tabs[tIndex];
    tab.m_finished = true;
    QString title = genericTitle(tab);
    m_tabBar->setTabText(tIndex, title);
    if (game == m_game)
    {
        // Finished tournament games are destroyed immediately
        // so we can't touch the game object any more.
        if (tab.m_tournament)
            m_game = nullptr;
        updateWindowTitle();
        updateMenus();
    }

    // save game notation of non-tournament games to default PGN file
    if (!tab.m_tournament
            &&  !game->pgn()->isNull()
            &&  	(  !game->pgn()->moves().isEmpty() // ignore empty games
                       || !game->pgn()->result().isNone())) // without adjudication
    {
        QString fileName = QSettings().value("games/default_pgn_output_file", QString())
                .toString();

        if (!fileName.isEmpty())
            game->pgn()->write(fileName);
        // TODO: reaction on error
    }
}

void MainWindow::OpenPgnGame()
{

    QString filePGN = QFileDialog::getOpenFileName(this,
                                                   tr("Open PGN"),
                                                   QString(),
                                                   tr("PGN Format (*.pgn);;All Files (*.*)"));


    QFile file(filePGN);
    QFileInfo fileInfo(filePGN);
    if (!fileInfo.exists())
    {
        QMessageBox::information(this, tr("Error"), tr("PGN file does not exist!"));
        return;
    }

    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        QMessageBox::information(this, tr("Error"), tr("PGN can't open properly!"));
        return;
    }

    PgnGame* pgnGame = new PgnGame();
    {
        PgnStream in(&file);
        if (!pgnGame->read(in)) {
            return;
        }
    }

    QString variant = m_game.isNull() || m_game->board() == nullptr ?
                "standard" : m_game->board()->variant();

    auto board = Chess::BoardFactory::create(variant);
    board->setFenString(board->defaultFenString());

    auto game = new ChessGame(board, pgnGame);
    game->setTimeControl(TimeControl("inf"));
    game->pause();

    connect(game, &ChessGame::initialized, this, &MainWindow::addGame);
    connect(game, &ChessGame::startFailed, this, &MainWindow::onGameStartFailed);

    SylvanApplication::instance()->gameManager()->newGame(game,
                                                          new HumanBuilder(SylvanApplication::userName()),
                                                          new HumanBuilder(SylvanApplication::userName()));
}

void MainWindow::newTournament()
{
    NewTournamentDialog dlg(SylvanApplication::instance()->engineManager(), this);
    if (dlg.exec() != QDialog::Accepted)
        return;

    GameManager* manager = SylvanApplication::instance()->gameManager();

    Tournament* t = dlg.createTournament(manager);
    auto resultsDialog = SylvanApplication::instance()->tournamentResultsDialog();
    connect(t, SIGNAL(finished()),
            this, SLOT(onTournamentFinished()));
    connect(t, SIGNAL(gameStarted(ChessGame*, int, int, int)),
            this, SLOT(addGame(ChessGame*)));
    connect(t, SIGNAL(gameFinished(ChessGame*, int, int, int)),
            resultsDialog, SLOT(update()));
    connect(t, SIGNAL(gameFinished(ChessGame*, int, int, int)),
            this, SLOT(onGameFinished(ChessGame*)));
    t->start();

    connect(m_stopTournamentAct, &QAction::triggered, [=]()
    {
        auto btn = QMessageBox::question(this, tr("Stop Tournament"),
                                         tr("Do you really want to stop the current tournament?"));
        if (btn != QMessageBox::Yes)
        {
            m_closing = false;
            return;
        }

        t->stop();
    });
    m_newTournamentAct->setEnabled(false);
    m_stopTournamentAct->setEnabled(true);
    resultsDialog->setTournament(t);
}

void MainWindow::onTournamentFinished()
{
    Tournament* tournament = qobject_cast<Tournament*>(QObject::sender());
    Q_ASSERT(tournament != nullptr);

    m_stopTournamentAct->disconnect();

    QString error = tournament->errorString();
    QString name = tournament->name();

    tournament->deleteLater();
    m_newTournamentAct->setEnabled(true);
    m_stopTournamentAct->setEnabled(false);

    if (m_closing)
    {
        closeAllGames();
        return;
    }

    m_showTournamentResultsAct->trigger();

    if (!error.isEmpty())
    {
        QMessageBox::critical(this,
                              tr("Tournament Error"),
                              tr("Tournament \"%1\" finished with an error.\n\n%2")
                              .arg(name, error));
    }

    SylvanApplication::alert(this);
}

void MainWindow::onWindowMenuAboutToShow()
{
    m_windowMenu->clear();

    addDefaultWindowMenu();
    m_windowMenu->addSeparator();

    const QList<MainWindow*> gameWindows =
            SylvanApplication::instance()->gameWindows();

    for (int i = 0; i < gameWindows.size(); i++)
    {
        MainWindow* gameWindow = gameWindows.at(i);

        QAction* showWindowAction = m_windowMenu->addAction(
                    gameWindow->windowListTitle(), this, SLOT(showGameWindow()));
        showWindowAction->setData(i);
        showWindowAction->setCheckable(true);

        if (gameWindow == this)
            showWindowAction->setChecked(true);
    }
}

void MainWindow::showGameWindow()
{
    if (QAction* action = qobject_cast<QAction*>(sender()))
        SylvanApplication::instance()->showGameWindow(action->data().toInt());
}

void MainWindow::updateWindowTitle()
{
    // setWindowTitle() requires "[*]" (see docs)
    const TabData& gameData(m_tabs.at(m_tabBar->currentIndex()));
    setWindowTitle(genericTitle(gameData) + QLatin1String("[*]"));
}

QString MainWindow::windowListTitle() const
{
    const TabData& gameData(m_tabs.at(m_tabBar->currentIndex()));

#ifndef Q_OS_MAC
    if (isWindowModified())
        return genericTitle(gameData) + QLatin1String("*");
#endif

    return genericTitle(gameData);
}

QString MainWindow::genericTitle(const TabData& gameData) const
{
    QString red;
    QString black;
    Chess::Result result;
    if (gameData.m_game)
    {
        red = gameData.m_game->player(Chess::Side::Red)->name();
        black = gameData.m_game->player(Chess::Side::Black)->name();
        result = gameData.m_game->result();
    }
    else
    {
        red = gameData.m_pgn->playerName(Chess::Side::Red);
        black = gameData.m_pgn->playerName(Chess::Side::Black);
        result = gameData.m_pgn->result();
    }

    if (result.isNone())
        return tr("%1 vs %2").arg(red, black);
    else
        return tr("%1 vs %2 (%3)")
                .arg(red, black, result.toShortString());
}

void MainWindow::updateMenus()
{
    QPointer<ChessPlayer> red = m_players[Chess::Side::Red];
    QPointer<ChessPlayer> black = m_players[Chess::Side::Black];
    bool isHumanGame =  (!red.isNull() && red->isHuman())
            || (!black.isNull() && black->isHuman());
    bool gameOn = (!m_game.isNull() && !m_game->isFinished());
    m_adjudicateBlackWinAct->setEnabled(gameOn);
    m_adjudicateRedWinAct->setEnabled(gameOn);
    m_adjudicateDrawAct->setEnabled(gameOn);
    m_resignGameAct->setEnabled(gameOn && isHumanGame);
}

QString MainWindow::nameOnClock(const QString& name, Chess::Side side) const
{
    QString text = name;
    bool displaySide = QSettings().value("ui/display_players_sides_on_clocks", false)
            .toBool();
    if (displaySide)
        text.append(QString(" (%1)").arg(side.toString()));
    return text;
}

void MainWindow::editMoveComment(int ply, const QString& comment)
{
    bool ok;
    QString text = QInputDialog::getMultiLineText(this, tr("Edit move comment"),
                                                  tr("Comment:"), comment, &ok);
    if (ok && text != comment)
    {
        lockCurrentGame();
        PgnGame* pgn(m_tabs.at(m_tabBar->currentIndex()).m_pgn);
        PgnGame::MoveData md(pgn->moves().at(ply));
        md.comment = text;
        pgn->setMove(ply, md);
        unlockCurrentGame();

        m_moveList->setMove(ply, md.move, md.moveString, text);
    }
}

void MainWindow::copyFen()
{
    QClipboard* cb = SylvanApplication::clipboard();
    QString fen(m_gameViewer->board()->fenString());
    if (!fen.isEmpty())
        cb->setText(fen);
}

void MainWindow::pasteFen()
{
    auto cb = SylvanApplication::clipboard();
    if (cb->text().isEmpty())
        return;

    QString fen = cb->text().trimmed();
    QStringList stFList = fen.split("fen");
    if (stFList.length() > 1) {
        fen = stFList[1].trimmed();
    }

    QString variant = m_game.isNull() || m_game->board() == nullptr ?
                "standard" : m_game->board()->variant();

    auto board = Chess::BoardFactory::create(variant);
    if (!board->setFenString(fen))
    {
        QMessageBox msgBox(QMessageBox::Critical,
                           tr("FEN Error"),
                           tr("Invalid FEN string for the \"%1\" variant:")
                           .arg(variant),
                           QMessageBox::Ok, this);
        msgBox.setInformativeText(cb->text());
        msgBox.exec();

        delete board;
        return;
    }

    auto game = new ChessGame(board, new PgnGame());
    game->setTimeControl(TimeControl("inf"));
    game->setStartingFen(fen);
    game->pause();

    connect(game, &ChessGame::initialized, this, &MainWindow::addGame);
    connect(game, &ChessGame::startFailed, this, &MainWindow::onGameStartFailed);

    SylvanApplication::instance()->gameManager()->newGame(game,
                                                          new HumanBuilder(SylvanApplication::userName()),
                                                          new HumanBuilder(SylvanApplication::userName()));
}

void MainWindow::showAboutDialog()
{
    QString html;
    html += "<h3>" + QString(tr("Xiangqi GUI - Sylvan %1"))
            .arg(SylvanApplication::applicationVersion()) + "</h3>";
    html += "<p>" + tr("Qt Version %1").arg(qVersion()) + "</p>";
    html += "<p>" + tr("Copyright 2019 ") + "</p>";
    html += "<p>" + tr("Author: Wilbert Lee") + "</p>";
    QMessageBox::about(this, tr("About"), html);
}

void MainWindow::lockCurrentGame()
{
    if (m_game != nullptr)
        m_game->lockThread();
}

void MainWindow::unlockCurrentGame()
{
    if (m_game != nullptr)
        m_game->unlockThread();
}

bool MainWindow::save()
{
    if (m_currentFile.isEmpty())
        return saveAs();

    return saveGame(m_currentFile);
}

bool MainWindow::saveAs()
{
    const QString fileName = QFileDialog::getSaveFileName(
                this,
                tr("Save game"),
                QString(),
                tr("PGN Format (*.pgn);;All Files (*.*)"),
                nullptr,
                QFileDialog::DontConfirmOverwrite);
    if (fileName.isEmpty())
        return false;

    return saveGame(fileName);
}

bool MainWindow::saveGame(const QString& fileName)
{
    lockCurrentGame();
    bool ok = m_tabs.at(m_tabBar->currentIndex()).m_pgn->write(fileName);
    unlockCurrentGame();

    if (!ok)
        return false;

    m_currentFile = fileName;
    setWindowModified(false);

    return true;
}

void MainWindow::copyPgn()
{
    QString str("");
    QTextStream s(&str);
    PgnGame* pgn = m_tabs.at(m_tabBar->currentIndex()).m_pgn;
    if (pgn == nullptr)
        return;
    s << *pgn;

    QClipboard* cb = SylvanApplication::clipboard();
    cb->setText(s.readAll());
}

void MainWindow::onGameManagerFinished()
{
    m_readyToClose = true;
    close();
}

void MainWindow::closeAllGames()
{
    auto app = SylvanApplication::instance();
    app->closeDialogs();

    for (int i = m_tabs.size() - 1; i >= 0; i--)
        closeTab(i);

    if (m_tabs.isEmpty())
        app->gameManager()->finish();
}

void MainWindow::closeEvent(QCloseEvent* event)
{
    if (m_readyToClose)
    {
        writeSettings();
        return QMainWindow::closeEvent(event); // clazy:exclude=returning-void-expression
    }

    if (askToSave())
    {
        m_closing = true;

        if (m_stopTournamentAct->isEnabled())
            m_stopTournamentAct->trigger();
        else
            closeAllGames();
    }

    event->ignore();
}

bool MainWindow::askToSave()
{
    if (isWindowModified())
    {
        QMessageBox::StandardButton result;
        result = QMessageBox::warning(this, QApplication::applicationName(),
                                      tr("Game changed.\nDo you want to save?"),
                                      QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);

        if (result == QMessageBox::Save)
            return save();
        else if (result == QMessageBox::Cancel)
            return false;
    }
    return true;
}

void MainWindow::adjudicateDraw()
{
    adjudicateGame(Chess::Side::NoSide);
}

void MainWindow::adjudicateRedWin()
{
    adjudicateGame(Chess::Side::Red);
}

void MainWindow::adjudicateBlackWin()
{
    adjudicateGame(Chess::Side::Black);
}

void MainWindow::adjudicateGame(Chess::Side winner)
{
    if (!m_game)
        return;

    auto result = Chess::Result(Chess::Result::Adjudication,
                                winner,
                                tr("User decision"));
    QMetaObject::invokeMethod(m_game, "onAdjudication",
                              Qt::QueuedConnection,
                              Q_ARG(Chess::Result, result));
}

void MainWindow::resignGame()
{
    if (m_game.isNull() || m_game->isFinished())
        return;

    ChessPlayer * player = m_game->playerToMove();
    if (!player->isHuman())
    {
        player = m_game->playerToWait();
        if (!player->isHuman())
            return;
    }
    Chess::Side side = player->side();
    auto result = Chess::Result(Chess::Result::Resignation,
                                side.opposite());
    QMetaObject::invokeMethod(m_game, "onResignation",
                              Qt::QueuedConnection,
                              Q_ARG(Chess::Result, result));
}

void MainWindow::onEngineAnalyze()
{
    // TODO
}

void MainWindow::onLinkChessboard()
{
    // TODO
}

void MainWindow::addDefaultWindowMenu()
{
    m_windowMenu->addAction(m_minimizeAct);
    m_windowMenu->addSeparator();
    m_windowMenu->addAction(m_showGameWallAct);
    m_windowMenu->addSeparator();
    m_windowMenu->addAction(m_showPreviousTabAct);
    m_windowMenu->addAction(m_showNextTabAct);
}
